/*
 * Copyright (c) 2019.
 * Private license for beyondts.
 * Contact beyondts for using, beyondts999@163.com
 */

package net.beyondts.mplug;

import net.beyondts.mplug.utils.BeanFactory;
import net.beyondts.mplug.utils.FileUtils;
import net.beyondts.mplug.utils.JarUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.web.bind.annotation.*;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * Project: beyondts-projects<br/>
 * Description: The core class of MPLUG framework, which manages all plugins.<br/>
 *              The manager provide a "loadAll" method to load all enabled plugins, usually used while starting project.
 * Created at: 2019-1-11<br/>
 * Owned by beyondts.
 *
 * @author blazecrystal
 * @version 1.0.0
 */
@RestController("PluginManager")
@RequestMapping("/plugins")
public class PluginManager {
    private static Logger LOGGER = LoggerFactory.getLogger(PluginManager.class);

    private String pluginsDir;
    private Map<String, PluginInfo> plugins;
    private PluginsConfig config;

    private Map<String, PluginInfo> defaultPlugins;
    private PluginsConfig defaultConfig;

    private Map<String, PluginInfo> removedPlugins;

    public PluginManager() throws IOException {
        this.pluginsDir = PluginsUtils.getPluginsDir();
        defaultPlugins = new HashMap<>();
        plugins = new HashMap<>();
        removedPlugins = new HashMap<>();
    }

    /**
     * Load all plugins default and outside.
     * @throws IOException ...
     * @throws ClassNotFoundException ..
     */
    public void loadAll() throws IOException, ClassNotFoundException {
        loadDefault();
        load();
    }

    /**
     * Load default plugins.
     * @throws IOException
     * @throws ClassNotFoundException
     */
    public void loadDefault() throws IOException, ClassNotFoundException {
        defaultConfig = PluginsConfig.readDefaultConfig();
        for (String plugin : defaultConfig.getEnabledPlugins()) {
            loadOnePlugin(defaultPlugins, plugin);
        }
        LOGGER.debug(defaultConfig.getEnabledPlugins().size() + " default plugins loaded");
    }

    // each plugin jar has its own ClassLoader, thus, each one can be unloaded by close its ClassLoader.

    /**
     * Load outside plugins.
     * @throws IOException
     * @throws ClassNotFoundException
     */
    public void load() throws IOException, ClassNotFoundException {
        config = PluginsConfig.readConfig(pluginsDir);
        for (String plugin : config.getEnabledPlugins()) {
            loadOnePlugin(plugins, plugin);
        }
        LOGGER.debug(config.getEnabledPlugins().size() + " outside plugins loaded");
    }

    /**
     * To list all enabled plugins (default and outside). This is also a RESTful interface.
     * @return Map&lt;String, List&lt;String&gt;&gt; enabled plugins list
     */
    @RequestMapping(value = "/get/all", method = RequestMethod.GET)
    @ResponseBody
    public Map<String, List<String>> getAllEnabledPlugins() {
        Map<String, List<String>> all = new HashMap<>();
        all.put("default", defaultConfig.getEnabledPlugins());
        all.put("outside", config.getEnabledPlugins());
        return all;
    }

    /**
     * To get the information of given plugin. The given plugin should be current enabled. This is also a RESTful interface.
     * @param plugin String the plugin name and version, format is name-version, just the part of the main jar's name
     * @return PluginInfo the information of plugin
     */
    @RequestMapping(value = "/get/{plugin}", method = RequestMethod.GET)
    @ResponseBody
    public PluginInfo getPluginInfo(@PathVariable String plugin) {
        PluginInfo info = plugins.get(plugin);
        if (info == null) {
            info = defaultPlugins.get(plugin);
        }
        return info;
    }

    /**
     * Install a new plugin (not exists in current enabled list). The operation needn't restart process. This is also a RESTful interface.
     * @param plugin String the plugin name and version, format is name-version, just the part of the main jar's name
     * @throws IOException ...
     * @throws ClassNotFoundException ...
     */
    @RequestMapping(value = "/install/{plugin}", method = RequestMethod.PUT)
    public void install(@PathVariable String plugin) throws IOException, ClassNotFoundException {
        if (plugins.containsKey(plugin)) {
            return;
        } else if (removedPlugins.containsKey(plugin)) {
            plugins.put(plugin, removedPlugins.remove(plugin));
            executeInstallSqls(plugins.get(plugin));
        } else {
            loadOnePlugin(plugins, plugin);
        }
        config.addPluginToConfig(plugins.get(plugin));
        LOGGER.debug("plugin \"" + plugin + "\" installed successfully");
    }

    /**
     * Uninstall a plugin (already exists in current enabled list). The operation needn't restart process. This is also a RESTful interface.
     * @param plugin String the plugin name and version, format is name-version, just the part of the main jar's name
     * @throws IOException ...
     */
    @RequestMapping(value = "/uninstall/{plugin}", method = RequestMethod.DELETE)
    public void uninstall(@PathVariable String plugin) throws IOException {
        if (!plugins.containsKey(plugin)) {
            return;
        }
        config.removePluginFromConfig(unloadOnePlugin(plugin));
//        System.gc(); // suggest GC to release unused PluginLoader
        LOGGER.debug("plugin \"" + plugin + "\" uninstalled successfully");
    }

    /**
     * Find the plugin method according to the extension's id.
     * @param extension String id of an extension
     * @return Method plugin method for the extension
     */
    public List<Method> findPluginMethod(String extension) {
        // first from outside plugins, then from default plugins. that means default plugsins will be replaced by outside.
        List<Method> methods = findPluginMethod(plugins, extension);
        if (methods == null) {
            methods = findPluginMethod(defaultPlugins, extension);
        }
        return methods;
    }

    private List<Method> findPluginMethod(Map<String, PluginInfo> pluginsContainer, String extension) {
        Iterator<PluginInfo> infos = pluginsContainer.values().iterator();
        while(infos.hasNext()) {
            PluginInfo info = infos.next();
            List<Method> methods = info.getPluginMethod(extension);
            if (methods != null) {
                return methods;
            }
        }
        return null;
    }

    private void loadOnePlugin(Map<String, PluginInfo> pluginsContainer, String plugin) throws IOException, ClassNotFoundException {
        // newly load a plugin completely
        String pluginDir = pluginsDir + "jars" + File.separator + plugin + File.separator;
        List<File> tmp = FileUtils.listFiles(pluginDir, "*.jar", false, false);
        if (tmp == null || tmp.size() < 1) {
            return;
        }
        File mainJar = tmp.get(0);  // just use first jar if more than one appeared here
        PluginInfo info = PluginInfo.load(mainJar.getAbsolutePath());
        tmp = FileUtils.listFiles(pluginDir + "lib", "*.jar", false, true);
        for (File jar : tmp) {
            info.getLoader().add(jar.getAbsolutePath());
        }
        pluginsContainer.put(plugin, info);
        executeInstallSqls(info);
        // regist beans in plugin to spring context
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext();
        ac.register(JarUtils.getComponentAnnotatedClasses(mainJar, info.getLoader()).toArray(new Class<?>[0]));
        ac.setParent(BeanFactory.getContext());
        ac.refresh();
        ac.start();
        BeanFactory.initByApplicationContext(ac);
    }

    private void executeInstallSqls(PluginInfo info) {
        SqlExecutor sqlExecutor = BeanFactory.getBean(SqlExecutor.class);
        if (sqlExecutor != null && info.getInstallSqls() != null && info.getInstallSqls().size() > 0) {
            sqlExecutor.executeSqls(info.getInstallSqls().toArray(new String[0]));
        }
    }

    private PluginInfo unloadOnePlugin(String plugin) throws IOException {
//        plugins.get(plugin).getLoader().close();
//        pluginContexts.get(plugin).close();
//        pluginContexts.remove(plugin);
        PluginInfo removed =  plugins.remove(plugin);
        removedPlugins.put(plugin, removed);
        executeUninstallSqls(removed);
        return removed;
    }

    private void executeUninstallSqls(PluginInfo info) {
        SqlExecutor sqlExecutor = BeanFactory.getBean(SqlExecutor.class);
        if (sqlExecutor != null && info.getUninstallSqls() != null && info.getUninstallSqls().size() > 0) {
            sqlExecutor.executeSqls(info.getUninstallSqls().toArray(new String[0]));
        }
    }
}
